﻿// ===============================================================================
//  Copyright © ymind.net. All rights reserved .
//  官方网站：http://ymind.net/
//  版权所有：彦铭工作室 老陈出品
// ===============================================================================

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Web;
using Lcsoft.TemplateEngine.Core;

namespace Lcsoft.TemplateEngine.XHtmlParser
{
	/// <summary>
	/// 表示 xhtml 文档解析器。
	/// </summary>
	[ComVisible(true)]
	[Serializable]
	internal sealed class XHtmlParser
	{
		private readonly string _initialPath;
		private readonly string _statementElementPrefix;
		private readonly TextReader _textReader;

		private int _column;
		private int _line;

		/// <summary>
		/// 初始化 <see cref="XHtmlParser"/> 类的新实例。
		/// </summary>
		/// <param name="templateElementPrefix">指定模板语句标签的前缀名。</param>
		/// <param name="initialPath">指定模板文件的初始路径。</param>
		/// <param name="reader">指定包含模板文档的 <see cref="TextReader"/>。</param>
		/// <param name="templateTags">模板引擎能够处理的语句标签的类型的集合。</param>
		public XHtmlParser(string templateElementPrefix, string initialPath, TextReader reader, SafeDictionary<string, Type> templateTags)
		{
			if (templateElementPrefix == null) throw new ArgumentNullException("templateElementPrefix");
			if (templateElementPrefix.Length == 0) throw new ArgumentOutOfRangeException("templateElementPrefix");

			if (initialPath == null) throw new ArgumentNullException("initialPath");
			if (initialPath.Length == 0) throw new ArgumentOutOfRangeException("initialPath");
			if (Path.IsPathRooted(initialPath) == false) throw new ArgumentException("参数 initialPath 必须是绝对路径。", "initialPath");
			if (Directory.Exists(initialPath) == false) throw new DirectoryNotFoundException(String.Format(@"无法找到指定路径：""{0}""。", initialPath));

			if (reader == null) throw new ArgumentNullException("reader");

			if (templateTags == null) throw new ArgumentNullException("templateTags");
			if (templateTags.Count == 0) throw new ArgumentOutOfRangeException("templateTags");

			this._initialPath = initialPath;
			this._statementElementPrefix = String.IsNullOrEmpty(templateElementPrefix) ? "mte" : templateElementPrefix;
			this._textReader = reader;
			this.TemplateTags = templateTags;
		}

		/// <summary>
		/// 获取模板引擎能够处理的语句标签的类型的集合。
		/// </summary>
		public SafeDictionary<string, Type> TemplateTags { get; private set; }

		private static void _MoveNodesDown(ref XHtmlNodeCollection nodes, int index, XHtmlElement newParent)
		{
			if (nodes == null) return;

			var count = nodes.Count;

			for (var i = index; i < count; i++)
			{
				newParent.Nodes.Add(nodes[i]);
				nodes[i].SetParent(newParent);
			}

			for (var i = index; i < count; i++) nodes.RemoveAt(index);

			newParent.IsExplicitlyTerminated = true;
		}

		private int _FindTagOpenNodeIndex(XHtmlNodeCollection nodes, string name)
		{
			if (nodes == null || nodes.Count == 0) return -1;
			if (String.IsNullOrEmpty(name)) return -1;

			for (var index = nodes.Count - 1; index >= 0; index--)
			{
				var element = nodes[index] as XHtmlElement;

				if (element == null) continue;

				if ((String.Compare(element.Name, name, true, CultureInfo.InvariantCulture) == 0 || String.Compare(String.Format("{0}:{1}", this._statementElementPrefix, element.Name), name, true, CultureInfo.InvariantCulture) == 0) && element.Nodes.Count == 0 && !element.IsTerminated) return index;
			}

			return -1;
		}

		private XHtmlNodeCollection _BuildNodeCollection(Queue<XHtmlToken> tokens)
		{
			var nodes = new XHtmlNodeCollection(null);

			while (tokens.Count > 0)
			{
				var currentToken = tokens.Dequeue();
				var current = currentToken.Data;

				switch (current)
				{
					case ("<"):
						if (tokens.Count == 0) break;

						current = tokens.Dequeue().Data;

						if (current.StartsWith("!--") && current.EndsWith("--"))
						{
							if (current.StartsWith(@"!--#include file=""") && current.EndsWith(@""" --"))
							{
								var file = current.Substring(18);

								file = file.Remove(file.IndexOf('"'));

								var ssiElement = new XHtmlSSIElement(this._initialPath, file, currentToken.Line, currentToken.Column);

								nodes.Add(ssiElement);
							}
							else
							{
								var comment = new XHtmlComment(String.Format("<{0}>", current), currentToken.Line, currentToken.Column);

								nodes.Add(comment);
							}

							break;
						}

						XHtmlElement element;

						if (current.StartsWith(String.Format("{0}:", this._statementElementPrefix)))
						{
							var tagName = current.Remove(0, current.IndexOf(':') + 1).ToLower();

							if (this.TemplateTags.ContainsKey(tagName)) element = Activator.CreateInstance(this.TemplateTags[tagName], new object[] { this._statementElementPrefix, currentToken.Line, currentToken.Column }) as XHtmlElement;
							else throw new ParseException("解析异常", String.Format("检测到未受支持的模板语句标签：{0}, 行：{1}, 列：{2}", current, currentToken.Line, currentToken.Column));
						}
						else
						{
							switch (current.ToLower())
							{
								case "link":
									element = new XHtmlLinkElement(currentToken.Line, currentToken.Column);
									break;
								case "script":
									element = new XHtmlScriptElement(currentToken.Line, currentToken.Column);
									break;
								default:
									element = new XHtmlElement(current, currentToken.Line, currentToken.Column);
									break;
							}
						}

						if (element == null) throw new ParseException("解析异常", String.Format("未能创建合适的 XHTML 标记的实例：{0}。", current));

						while (tokens.Count > 0 && (current = tokens.Dequeue().Data) != ">" && current != "/>")
						{
							var attributeName = current;

							if (tokens.Count > 0 && tokens.Peek().Data == "=")
							{
								tokens.Dequeue();

								current = (tokens.Count > 0) ? tokens.Dequeue().Data : null;

								var attribute = new XHtmlAttribute(attributeName, HttpUtility.HtmlDecode(current));
								element.Attributes.Add(attribute.Name, attribute);
							}
							else
							{
								var attribute = new XHtmlAttribute(attributeName);
								element.Attributes.Add(attribute.Name, attribute);
							}
						}

						nodes.Add(element);

						switch (current)
						{
							case "/>":
								element.IsTerminated = true;

								break;
							case ">":
								continue;
						}

						break;
					case (">"):
						continue;
					case ("</"):
						if (tokens.Count == 0) break;

						current = tokens.Dequeue().Data;

						var openIndex = this._FindTagOpenNodeIndex(nodes, current);

						if (openIndex != -1) _MoveNodesDown(ref nodes, openIndex + 1, (XHtmlElement)nodes[openIndex]);

						while (tokens.Count > 0 && (tokens.Dequeue().Data) != ">") {}

						break;
					default:
						var node = new XHtmlText(current, currentToken.Line, currentToken.Column);

						nodes.Add(node);

						break;
				}
			}

			return nodes;
		}

		private XHtmlToken _CreateToken(string value) { return new XHtmlToken(value, this._line, this._column); }

		private Queue<XHtmlToken> _GetTokens(TextReader reader)
		{
			var builder = new StringBuilder();
			var tokens = new Queue<XHtmlToken>();
			var parserState = new Stack<XHtmlParserStatus>();
			var status = XHtmlParserStatus.Text;
			parserState.Push(XHtmlParserStatus.Text);

			var inScript = false;
			var inStyle = false;

			var actualChar = 0;

			this._line = 1;
			this._column = 1;

			while (actualChar > -1)
			{
				actualChar = reader.Read();

				if (actualChar == -1) break;

				this._column++;

				switch ((Char)actualChar)
				{
					case '\n':
					case '\r':
						this._line++;
						this._column = 1;

						reader.Read();
						builder.AppendLine();

						continue;
				}

				switch (status)
				{
					case XHtmlParserStatus.InsideDoubleQuotes:

						builder.Append((char)actualChar);

						if (actualChar == '"')
						{
							status = parserState.Pop();

							if (status == XHtmlParserStatus.AttributeValue)
							{
								tokens.Enqueue(this._CreateToken(builder.ToString()));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Attribute;
							}
						}

						break;

					case XHtmlParserStatus.InsideSingleQuotes:

						builder.Append((char)actualChar);

						if (actualChar == '\'')
						{
							status = parserState.Pop();

							if (status == XHtmlParserStatus.AttributeValue)
							{
								tokens.Enqueue(this._CreateToken(builder.ToString()));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Attribute;
							}
						}

						break;

					case XHtmlParserStatus.Text:

						if (inScript)
						{
							if (actualChar == '<' && reader.Peek() == '/')
							{
								reader.Read();

								var stack = new StringBuilder();
								const string script = "script";

								for (var i = script.Length; i > 0; i--)
								{
									actualChar = reader.Read();

									if (char.ToLower((char)actualChar, CultureInfo.InvariantCulture) != script[script.Length - i])
									{
										builder.Append("</");
										stack.Append((char)actualChar);
										builder.Append(stack);

										inScript = false;

										break;
									}

									stack.Append((char)actualChar);
								}

								if (inScript)
								{
									if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

									tokens.Enqueue(this._CreateToken("</"));

									builder = new StringBuilder(script);

									status = XHtmlParserStatus.Tag;

									inScript = false;
								}
								else inScript = true;
							}
							else
							{
								builder.Append((char)actualChar);

								break;
							}

							break;
						}

						if (inStyle)
						{
							if (actualChar == '<' && reader.Peek() == '/')
							{
								reader.Read();

								var stack = new StringBuilder();
								const string style = "style";

								for (var i = style.Length; i > 0; i--)
								{
									actualChar = reader.Read();

									if (char.ToLower((char)actualChar, CultureInfo.InvariantCulture) != style[style.Length - i])
									{
										builder.Append("</");
										stack.Append((char)actualChar);
										builder.Append(stack);

										inStyle = false;

										break;
									}

									stack.Append((char)actualChar);
								}

								if (inStyle)
								{
									if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

									tokens.Enqueue(this._CreateToken("</"));

									builder = new StringBuilder(style);

									status = XHtmlParserStatus.Tag;

									inStyle = false;
								}
								else inStyle = true;
							}
							else
							{
								builder.Append((char)actualChar);

								break;
							}

							break;
						}

						switch ((char)actualChar)
						{
							case '<':
								if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

								status = XHtmlParserStatus.TagStart;

								if (reader.Peek() == '/')
								{
									reader.Read();

									tokens.Enqueue(this._CreateToken("</"));
								}
								else tokens.Enqueue(this._CreateToken("<"));

								builder = new StringBuilder();

								break;
							default:

								builder.Append((char)actualChar);

								break;
						}

						break;

					case XHtmlParserStatus.TagStart:

						switch ((char)actualChar)
						{
							case ' ':
							case '\t':
							case '\r':
							case '\n':
								break;
							case '!':

								status = XHtmlParserStatus.SgmlComment;

								builder.Append((char)actualChar);

								break;
							case '>':
								throw new ParseException("解析异常", "在解析模板代码时遇到非法字符“>”。");
							default:
								status = XHtmlParserStatus.Tag;
								builder = new StringBuilder();

								builder.Append((char)actualChar);

								break;
						}

						break;

					case XHtmlParserStatus.Comment:
					case XHtmlParserStatus.SgmlComment:

						switch ((char)actualChar)
						{
							case '>':
								if (status == XHtmlParserStatus.SgmlComment)
								{
									tokens.Enqueue(this._CreateToken(builder.ToString()));

									tokens.Enqueue(this._CreateToken(">"));

									builder = new StringBuilder();

									status = XHtmlParserStatus.Text;
								}
								else builder.Append((char)actualChar);

								break;
							case '-':
								if (status == XHtmlParserStatus.SgmlComment && reader.Peek() == '-')
								{
									reader.Read();

									status = XHtmlParserStatus.Comment;

									builder.Append('-');
								}
								else if (status == XHtmlParserStatus.Comment && reader.Peek() == '-')
								{
									reader.Read();

									status = XHtmlParserStatus.SgmlComment;

									builder.Append('-');
								}

								builder.Append((char)actualChar);

								break;
							default:
								builder.Append((char)actualChar);

								break;
						}

						break;

					case XHtmlParserStatus.Tag:

						switch ((char)actualChar)
						{
							case ' ':
							case '\t':
							case '\r':
							case '\n':
								var current = builder.ToString();

								status = XHtmlParserStatus.Attribute;

								tokens.Enqueue(this._CreateToken(current));

								builder = new StringBuilder();

								if ("script".Equals(current.ToLower(CultureInfo.InvariantCulture))) inScript = true;
								if ("style".Equals(current.ToLower(CultureInfo.InvariantCulture))) inStyle = true;

								break;
							case '>':
								tokens.Enqueue(this._CreateToken(builder.ToString()));
								tokens.Enqueue(this._CreateToken(">"));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Text;

								break;
							case '/':
								tokens.Enqueue(this._CreateToken(builder.ToString()));

								builder = new StringBuilder();

								builder.Append((char)actualChar);

								status = XHtmlParserStatus.TagEnd;

								break;
							default:
								builder.Append((char)actualChar);

								break;
						}

						break;

					case XHtmlParserStatus.TagEnd:

						if (actualChar == '>')
						{
							builder.Append((char)actualChar);
							tokens.Enqueue(this._CreateToken(builder.ToString()));

							builder = new StringBuilder();

							status = XHtmlParserStatus.Text;
						}

						break;

					case XHtmlParserStatus.Attribute:

						switch ((char)actualChar)
						{
							case ' ':
							case '\t':
							case '\r':
							case '\n':
								status = XHtmlParserStatus.Attribute;

								if (builder.Length > 0)
								{
									tokens.Enqueue(this._CreateToken(builder.ToString()));

									builder = new StringBuilder();
								}

								break;
							case '>':
								if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

								tokens.Enqueue(this._CreateToken(">"));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Text;

								break;
							case '/':
								if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

								builder = new StringBuilder();

								builder.Append((char)actualChar);

								status = XHtmlParserStatus.TagEnd;

								break;
							case '=':
								tokens.Enqueue(this._CreateToken(builder.ToString()));
								tokens.Enqueue(this._CreateToken("="));

								builder = new StringBuilder();

								status = XHtmlParserStatus.AttributeValue;

								break;
							default:
								builder.Append((char)actualChar);

								break;
						}

						break;

					case XHtmlParserStatus.AttributeValue:

						switch ((char)actualChar)
						{
							case ' ':
							case '\t':
							case '\r':
							case '\n':
								if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Attribute;

								break;
							case '>':
								if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

								tokens.Enqueue(this._CreateToken(">"));

								builder = new StringBuilder();

								status = XHtmlParserStatus.Text;

								break;
							case '/':
								if (reader.Peek() == '>')
								{
									if (builder.Length > 0) tokens.Enqueue(this._CreateToken(builder.ToString()));

									builder = new StringBuilder();

									builder.Append((char)actualChar);

									status = XHtmlParserStatus.TagEnd;
								}
								else builder.Append((char)actualChar);

								break;
							case '\'':
								builder.Append((char)actualChar);

								parserState.Push(status);

								status = XHtmlParserStatus.InsideSingleQuotes;

								break;
							case '"':
								builder.Append((char)actualChar);

								parserState.Push(status);

								status = XHtmlParserStatus.InsideDoubleQuotes;

								break;
							default:
								builder.Append((char)actualChar);

								break;
						}

						break;
				}
			}

			return tokens;
		}

		/// <summary>
		/// 返回正在分析的文档所包含的节点的集合。
		/// </summary>
		/// <returns>返回 <see cref="XHtmlNodeCollection"/>。</returns>
		public XHtmlNodeCollection GetNodes()
		{
			var tokens = this._GetTokens(this._textReader);

			return this._BuildNodeCollection(tokens);
		}
	}
}
